# template_funs.py

r'''This has the template functions.

Including reading and manipulating templates.
'''

from __future__ import with_statement, division

import math
import itertools
import sys      # for debugging

def frange(inc, *points):
    r'''Floating point range function.

    Goes from point to point, being sure to include each point in the output
    (+/- 0.001).  This may require a smaller final ``inc`` value.

        >>> tuple(frange(2.0, 0.0, 1.0))
        (0.0, 1.0)
        >>> tuple(frange(2.0, -4.5, 0.0))
        (-4.5, -2.5, -0.5, 0.0)
        >>> tuple(frange(-2.0, -4.5, 0.0))
        Traceback (most recent call last):
            ...
        ValueError: points out of order
        >>> tuple(frange(2.0, 4.5, 0.0))
        Traceback (most recent call last):
            ...
        ValueError: points out of order
    '''
    last_point = None
    for point in points:
        if last_point is not None:
            if inc > 0.0:
                if point < last_point:
                    raise ValueError("points out of order")
                while last_point + inc < point:
                    yield last_point
                    last_point += inc
                if last_point + 1e-3 < point: yield last_point
            else:
                if point > last_point:
                    raise ValueError("points out of order")
                while last_point + inc > point:
                    yield last_point
                    last_point += inc
                if last_point - 1e-3 > point: yield last_point
        last_point = point
    if last_point is not None:
        yield last_point

class template_fun(object):
    r'''This acts like a function which interpolates look-ups from a table.

    ``iterable`` yields x, y values that must be sorted on x.  This is the
    reference table.  A copy is made.
    '''
    def __init__(self, iterable):
        self.values = tuple((float(x), float(y)) for x, y in iterable)
        for i in range(len(self.values) - 1):
            assert self.values[i][0] < self.values[i+1][0], \
                   "template_fun: values not sorted on X: {0} {1}" \
                     .format(self.values[i][0], self.values[i+1][0])

    def __call__(self, x):
        #print >> sys.stderr, "template_fun.__call__", repr(x)
        x = float(x)

        # This could be caused by offsetting for a radius.
        if x < self.values[0][0]:
            assert self.values[0][0] - x < 0.1, \
                   "template_fun: %f < first template value" % x
            return self.values[0][1]

        # Do a binary search to find x.  Invariant: low <= x, high > x
        low, high = 0, len(self.values)
        while high > low + 1:
            middle = (high + low) // 2
            if self.values[middle][0] <= x: low = middle
            else: high = middle

        # This could be caused by offsetting for a radius.
        if low == len(self.values) - 1:
            assert x - self.values[-1][0] < 0.1, \
                   "template_fun: %f > last template value" % x
            return self.values[-1][1]

        x0, y0 = self.values[low]
        x1, y1 = self.values[low + 1]
        return y0 + ((y1 - y0) / (x1 - x0)) * (x - x0)


class repeating_template(template_fun):
    r'''A repeating template.  Chiefly used for rosettes.

    Note that this will repeat a pattern around a circle if you give it less
    than 360 degrees.  For example giving it a pattern that goes from 0 to 60
    degrees would result in that pattern being repeated 6 times around the
    full circle.
    '''

    def __init__(self, iterable):
        template_fun.__init__(self, iterable)
        self.min_x = min(x for x, y in self.values)
        self.max_x = max(x for x, y in self.values)
        self.length = self.max_x - self.min_x

    def __call__(self, x):
        x = x % self.length + self.min_x
        return super(repeating_template, self).__call__(x)


def file_single_num(filename):
    r'''Yields floats from ``filename``.

    One float per line.  Number is converted from text to python float.

    Use with :py:func:``step_x`` to add x values to get x, y values.
    '''
    with open(filename, 'r') as f:
        for line in f:
            line = line.strip()
            if line and line[0] not in '"#;\'':
                yield float(line)

def file_multi_num(filename, split=','):
    r'''Yields floats from ``filename``.

    Multiple floats per line separated by whitespace.  Numbers are converted
    from text to python floats.

    Floats are yielded as tuples, one per line.
    '''
    with open(filename, 'r') as f:
        for line in f:
            line = line.strip()
            if line and line[0] not in '"#;\'':
                ans =  tuple(float(x) for x in line.split(split))
                #print >> sys.stderr, "file_multi_num: yielding", ans
                yield ans

def step_x(step_size, iterable):
    r'''Yields (x, y) values.

    x starts at 0 and increments by ``step_size``.
    y from ``iterable``.
    '''
    x = 0.0
    for y in iterable:
        yield x, float(y)
        x += step_size

def mirror(seq, duplicate_first=False, flip_Y=False):
    r'''Mirrors a seq of x, y pairs.

    Does not duplicate the last element of seq.  Only duplicates the first
    element of seq if duplicate_first is True.  This allows sharp cusps at
    the ends.

    Inverts the Y values in the mirrored copy if flip_Y is true.

        >>> tuple(mirror(((1,2), (3,4), (5,6), (7, 8))))
        ((1, 2), (3, 4), (5, 6), (7, 8), (9, 6), (11, 4))
        >>> tuple(mirror(((1,2), (3,4), (5,6), (7, 8)), True))
        ((1, 2), (3, 4), (5, 6), (7, 8), (9, 6), (11, 4), (13, 2))
        >>> tuple(mirror(((1,2), (3,4), (5,6), (7, 8)), True, True))
        ((1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12), (13, 14))
    '''
    seq = tuple(seq)
    first_x, first_y = seq[0]
    last_x, last_y = seq[-1]
    if flip_Y:
        return itertools.chain(seq,
                               ((2 * last_x - x, 2 * last_y - y)
                                for x, y in reversed(seq[0:-1]
                                                     if duplicate_first
                                                     else seq[1:-1])))
    else:
        return itertools.chain(seq,
                               ((2 * last_x - x, y)
                                for x, y in reversed(seq[0:-1]
                                                     if duplicate_first
                                                     else seq[1:-1])))

def offset(delta_x, delta_y, radius, work_on_right = True):
    r'''Returns offsets to tool with radius ``radius`` from work.

    This assumes that the work is on the right side of the tool (from the
    direction of tool travel).
    '''
    delta = radius / math.hypot(delta_x, delta_y)
    if work_on_right:
        return delta * -delta_y, delta * delta_x
    return delta * delta_y, delta * -delta_x

def offset_template(radius, iterable, work_on_right = True):
    r'''Generates an tool offset table from ``iterable``.

    Yields x_with_offset, y_with_offset, orig_x

    ``iterable``      must be a sequence of x, y pairs sorted on x.
    ``radius``        is the tool radius.
    ``work_on_right`` True if the work is on the right side of the tool (in
                      the direction of tool travel)
    '''
    iterable = iter(iterable)
    current_x, current_y = iterable.next()
    try:
        next_x, next_y = iterable.next()
        delta_x, delta_y = \
            offset(next_x - current_x, next_y - current_y, radius,
                   work_on_right)
        try:
            while True:
                yield current_x + delta_x, current_y + delta_y, current_x
                prior_x, prior_y = current_x, current_y
                current_x, current_y = next_x, next_y
                next_x, next_y = iterable.next()
                delta_x, delta_y = \
                    offset(next_x - prior_x, next_y - prior_y, radius,
                           work_on_right)
        except StopIteration:
            delta_x, delta_y = \
                offset(current_x - prior_x, current_y - prior_y, radius,
                       work_on_right)
            yield current_x + delta_x, current_y + delta_y, current_x
    except StopIteration:
        if work_on_right:
            yield current_x, current_y + radius, current_x
        else:
            yield current_x, current_y - radius, current_x


def test():
    import doctest
    import sys
    sys.exit(doctest.testmod()[0])

if __name__ == "__main__":
    test()
